﻿using System;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Linq;
using System.Text;
using System.IO;
using System.Web;

using LINQToolKit.Web;
using LINQToolKit.Query;
using LINQToolKit.Mapping;
using LINQToolKit.Processors;
using LINQToolKit.Collections;
using LINQToolKit.Configuration;
using LINQToolKit.Query.Rules;

namespace LINQToolKit
{
    /// <summary>
    /// Represents the base class for the query provider
    /// </summary>
    /// <remarks>
    /// This framework is under the GNU licence and Creative Commons.
    /// You can also get a copy of the source code from google code
    /// <see cref="http://linqtodouban.googlecode.com/svn/trunk/"/>.
    /// For more information and further support, please contact with kevin (chwkai@gmail.com)
    /// </remarks>
    public class QueryProvider : IEntityProvider, IQueryProvider
    {
        private IMappingSource _mappingSource;
        private IRequest _request;
        private ProviderConfiguration _configuration;
        public TextWriter Log { get; set; }

        protected ProviderConfiguration Configuration
        {
            get { return this._configuration; }
        }

        public QueryProvider(IMappingSource mappingSource, IRequest request)
            : this(mappingSource, null, request)
        {
        }

        public QueryProvider(IMappingSource mappingSource, ProviderConfiguration config, IRequest request)
        {
            mappingSource.AssertNull("mappingSource");
            request.AssertNull("request");

            this._mappingSource = mappingSource;
            this._configuration = config;
            this._request = request;
        }

        /// <summary>
        /// Returns the mapping source for this provider
        /// </summary>
        protected IMappingSource MappingSource
        {
            get { return this._mappingSource; }
        }

        protected IRequest Request
        {
            get { return this._request; }
        }
        
        protected virtual void SendLog(string message)
        {
            if (this.Log != null)
            {
                this.Log.WriteLine(message);
            }
        }

        /// <summary>
        /// Returns an instance of processor for transforming the entity
        /// </summary>
        /// <param name="entityType"></param>
        /// <returns></returns>
        protected virtual IXmlEntityProcessor CreateProcessor(Type entityType)
        {
            IXmlEntityProcessor processor = null;
            var model = this.MappingSource.Entities[entityType];

            if (model.Processor == null)
            {
                if (this._configuration != null && this._configuration.Processor != null)
                {
                    processor = (IXmlEntityProcessor)Activator.CreateInstance(
                        this._configuration.Processor.Type);
                    processor.InjectProperties(this._configuration.Processor.Parameters);
                }
                else
                {
                    processor = new EntityProcessor();
                }
            }
            else
            {
                processor = (IXmlEntityProcessor)Activator.CreateInstance(model.Processor);
            }

            return processor;
        }

        /// <summary>
        /// To bind the association property with this provider
        /// </summary>
        private void BindAssociations(MetaEntity model, IEnumerable entities)
        {
            foreach (var ass in model.Associations)
            {
                PropertyInfo assProperty = model.Type.GetProperty(
                    ass.Name, BindingFlags.Instance | BindingFlags.Public);
                assProperty.AssertNull(ass.Name, "cannot find the association definition");
                PropertyInfo providerInfo = assProperty.PropertyType.GetProperty(
                    "Provider", BindingFlags.Public | BindingFlags.Instance);

                foreach (var item in entities)
                {
                    var assObj = assProperty.GetValue(item, null);
                    providerInfo.SetValue(assObj, this, null);
                }
            }
        }

        /// <summary>
        /// Perform the query request
        /// </summary>
        /// <param name="source"></param>
        /// <param name="throwOnErr"></param>
        /// <returns></returns>
        private RequestResult SelectRequest(Uri source, bool throwOnErr)
        {
            source.AssertNull("source");
            var encodedUrl = source.Encode();

            // output the query url
            this.SendLog(encodedUrl.ToString());
            var result = this.Request.ExecuteSelect(encodedUrl, string.Empty);
            // keep this value for association source
            result.RequestUrl = source;

            if (!result.Success)
            {
                throw new RequestException(result);
            }

            return result;
        }

        protected virtual IEnumerable CreateQuerySession(RequestResult rawResult, Type entityType, IEnumerable rawData)
        {
            return rawData;
        }

        /// <summary>
        /// Query the given source and return objects of the given type
        /// </summary>
        /// <param name="entityType"></param>
        /// <param name="source"></param>
        /// <returns></returns>
        public virtual IEnumerable Select(Type entityType, Uri source)
        {
            entityType.AssertNull("entityType");
            var requestResult = this.SelectRequest(source, true);
            var model = this.MappingSource.Entities[entityType];
            var entities = this.CreateProcessor(entityType).ConvertAll(model, requestResult.Message);

            if (entities != null)
            {
                this.BindAssociations(model, entities);
                return this.CreateQuerySession(requestResult, entityType, entities);    
            }

            return null;
        }

        /// <summary>
        /// Query the given source and return one object of the given type
        /// </summary>
        /// <param name="entityType"></param>
        /// <param name="source"></param>
        /// <returns></returns>
        public virtual object SelectOne(Type entityType, Uri source)
        {
            entityType.AssertNull("entityType");
            var requestResult = this.SelectRequest(source, true);
            var model = this.MappingSource.Entities[entityType];
            var entity = this.CreateProcessor(entityType).ConvertOne(model, requestResult.Message);
            // binding the association with the provider
            this.BindAssociations(model, new object[] { entity });

            return entity;
        }

        /// <summary>
        /// perform delete operation on the given source
        /// </summary>
        /// <param name="entityType"></param>
        /// <param name="source"></param>
        /// <returns></returns>
        public virtual RequestResult Delete(Type entityType, Uri source)
        {
            entityType.AssertNull("entityType");
            source.AssertNull("source");

            // dump logging
            this.SendLog(string.Format("DELETE:{0}", source));
            var result = this.Request.ExecuteDelete(source, string.Empty);
            return result;
        }

        /// <summary>
        /// perform insert operation on the given source
        /// </summary>
        /// <param name="source"></param>
        /// <param name="entity"></param>
        /// <returns></returns>
        public virtual RequestResult Insert(Uri source, object entity)
        {
            source.AssertNull("source");
            entity.AssertNull("entity");

            var entityType = entity.GetType();
            var post = this.CreateProcessor(entityType).ToInsertXml(
                this.MappingSource.Entities[entityType], entity);
            // dump logging
            this.SendLog(string.Format("INSERT:{0}", source));
            this.SendLog(post);
            var result = this.Request.ExecuteInsert(source, post);

            return result;
        }

        /// <summary>
        /// perform update operation on the given source
        /// </summary>
        /// <param name="source"></param>
        /// <param name="entity"></param>
        /// <returns></returns>
        public virtual RequestResult Update(Uri source, object entity)
        {
            source.AssertNull("source");
            entity.AssertNull("entity");

            var entityType = entity.GetType();
            var post = this.CreateProcessor(entityType).ToUpdateXml(
                this.MappingSource.Entities[entityType], entity);

            // dump logging
            this.SendLog(string.Format("UPDATE:{0}", source));
            this.SendLog(post);
            var result = this.Request.ExecuteUpdate(source, post);

            return result;
        }

        /// <summary>
        /// Returns an object of IQueryable for the given expression
        /// </summary>
        /// <typeparam name="TElement"></typeparam>
        /// <param name="expression"></param>
        /// <returns></returns>
        public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        {
            return new EntityQuery<TElement>(this, expression);
        }

        /// <summary>
        /// Returns an object of IQueryable for the given expression
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        public IQueryable CreateQuery(Expression expression)
        {
            Type entityType = this.GetElementType(expression);
            return (IQueryable)Activator.CreateInstance(
                typeof(EntityQuery<>).MakeGenericType(entityType), new object[] { this, expression }
            );
        }

        /// <summary>
        /// Execute the given expression
        /// </summary>
        /// <typeparam name="TResult"></typeparam>
        /// <param name="expression"></param>
        /// <returns></returns>
        public TResult Execute<TResult>(Expression expression)
        {
            var result = this.Execute(expression);

            if (result == null)
            {
                return default(TResult);
            }

            return (TResult)this.Execute(expression);
        }

        protected virtual IEnumerableExecutor GetEnumerableExecutor()
        {
            return new EnumerableExecutor();
        }

        /// <summary>
        /// Returns the expected type of entity of the given expression
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        private Type GetElementType(Expression expression)
        {
            if (expression.NodeType == ExpressionType.Call)
            {
                var method = (MethodCallExpression)expression;
                return method.Arguments[0].Type.GetGenericArguments()[0];
            }
            else
            {
                return expression.Type;
            }

            //if (expression.Type.IsGenericType && 
            //    typeof(IEnumerable).IsAssignableFrom(expression.Type))
            //{
            //    return expression.Type.GetGenericArguments()[0];
            //}
            //else
            //{
            //    return expression.Type;
            //}
        }

        /// <summary>
        /// Execute the given expression
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        public object Execute(Expression expression)
        {
            IList<string> queries = this.CreateTranslator().GetQueries(expression);

            if (queries == null || queries.Count == 0)
            {
                return null;
            }

            var entityType = this.GetElementType(expression);
            var model = this.MappingSource.Entities[entityType];
            var collection = (IEnumerableCollection)
                Activator.CreateInstance(typeof(Enumerables<>).MakeGenericType(entityType));

            foreach (var item in queries)
            {
                var result = this.SelectRequest(new Uri(item), false);

                if (result.Success)
                {
                    var entities = this.CreateProcessor(entityType).ConvertAll(model, result.Message);

                    if (entities != null)
                    {
                        //entities = this.DistinctEntities(entityType, entities);
                        this.BindAssociations(model, entities);
                        collection.Add(this.CreateQuerySession(result, entityType, entities));   
                    }
                }
            }

            return this.GetEnumerableExecutor().Execute(expression, collection);
        }

        //private IEnumerable DistinctEntities(Type entityType, IEnumerable entities)
        //{
        //    MetaEntity model = this.MappingSource.Entities[entityType];

        //    if (string.IsNullOrEmpty(model.PrimaryKey))
        //    {
        //        return entities;
        //    }

        //    var comparerType = typeof(PrimaryKeyComparer<>).MakeGenericType(entityType);
        //    MethodInfo method = EnumerableExecutor.GetEnumerableMethod(
        //        "Distinct",
        //        new Type[] { typeof(IEnumerable<>).MakeGenericType(entityType), comparerType}
        //        );

        //    var comparer = Activator.CreateInstance(comparerType, new object[] {model.PrimaryKey});
        //    return (IEnumerable)method.MakeGenericMethod(entityType).Invoke(
        //        null, new object[] { entities, comparer });
        //}

        /// <summary>
        /// Returns an instance of translator for translating the expression to queries
        /// </summary>
        /// <returns></returns>
        protected virtual ITranslator CreateTranslator()
        {
            ITranslator instance = null;

            if (this._configuration == null || this._configuration.Translator == null)
            {
                instance = new RuleTranslator();
            }
            else
            {
                instance = (ITranslator)
                        Activator.CreateInstance(this._configuration.Translator.Type);
                instance.InjectProperties(this._configuration.Translator.Parameters);
            }

            return instance;
        }

        /// <summary>
        /// Dispose the provider
        /// </summary>
        public virtual void Dispose()
        {
            // do nothing
        }
    }
}
